﻿#region

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Resources;
using System.Text;
using System.Xml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using Tum.CollabXT.DocxGen.Resources;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.VariantTypes;
using System.Reflection;
using TUM.Mustertexte_Parser;
using System.Threading;

#endregion

namespace Tum.CollabXT.DocxGen
{
    internal class DocumentGenerator
    {
        public const int MaxSubParagraphLevel = 5;


        public string OutputPath { get; set; }

        public string ProjectName { get; set; }

        public string Company
        {
            get;
            set;
        }

        public string Author
        {
            get;
            set;
        }

        public string TemplateDocxPath { get; set; }
        private TemplateConfig TemplateDocxConfig;

        public string TemplateDotxPath { get; set; }
        private TemplateConfig TemplateDotxConfig;

        public string TemplateTextsPath { get; set; }

        public const string ProductNameProperty = "PET_TemplateProductName";


        private readonly Log _Log;

        public DocumentGenerator(Log log)
        {
            if (log == null)
                throw new DocxGenException("Log must not be null");
            _Log = log;
        }

        /// <summary>
        /// Generates the document templates for all products with ids in set productsToBeGenerated.
        /// </summary>
        public void GenerateDocuments(IProcessProvider processProvider, Dictionary<string, Tree<TemplateSerializationItem>> productsToBeGenerated)
        {
            if (!(OutputPath.EndsWith("/") || OutputPath.EndsWith(@"\")))
                OutputPath += "/";

            //Create output folder
            try
            {
                if (!Directory.Exists(OutputPath))
                    Directory.CreateDirectory(OutputPath);
            }
            catch
            {
                throw new DocxGenException("Could not create output directory");
            }

            //Create config instances
            TemplateDocxConfig = new TemplateConfig(TemplateDocxPath);
            TemplateDotxConfig = new TemplateConfig(TemplateDotxPath);

            //Generate templates
            foreach (var curProdItem in productsToBeGenerated)
            {
                var curProd = processProvider.GetArtifactByID(curProdItem.Key);
                if (curProd == null)
                    continue;

                //Get product discipline association
                var productDisciplineAssoc = processProvider.GetAssociationsWithSource<IArtifact, IDiscipline>(curProd);

                //Copy product template
                var docX = curProd.IsInitial;
                var tempDocumentPath = CopyTemplate(OutputPath, docX);
                if (string.IsNullOrEmpty(tempDocumentPath))
                    return;


                try
                {
                    Stream stream;
                    int openTempStreamIteration = 0;
                    while(true)
                    {
                        try
                        {
                            stream = new FileStream(tempDocumentPath, FileMode.Open, FileAccess.ReadWrite);
                            break;
                        }
                        catch(IOException)
                        {
                        }
                        Thread.Sleep(10);
                        openTempStreamIteration++;
                        if(openTempStreamIteration > 100)
                        {
                            _Log.AddEntry("Could not open temporary document. Transformation stopped.", LogEntryType.Error);
                            return;
                        }
                    }
                    using (var wDocDocument = WordprocessingDocument.Open(stream, true))
                    {
                        //Write file properties
                        var coreProperties = wDocDocument.CoreFilePropertiesPart;
                        var packageProperties = coreProperties.OpenXmlPackage.PackageProperties;
                        packageProperties.Title = curProd.Name;
                        packageProperties.Creator = Author;

                        var extendedProperties = wDocDocument.ExtendedFilePropertiesPart;
                        extendedProperties.Properties.Company.Text = Company;
                        extendedProperties.Properties.Save();

                        //Write product name to file
                        SetDocumentProperty(wDocDocument, ProductNameProperty, curProd.Name);


                        var mainPart = wDocDocument.MainDocumentPart;

                        //Create custom tags
                        CreateTagAttributes(mainPart.Document);
                        foreach (HeaderPart curHeaderPart in mainPart.HeaderParts)
                            CreateTagAttributes(curHeaderPart.Header);


                        //Create utils
                        var docUtils = new DocumentUtils(mainPart);
                        docUtils.TemplateDocumentConfig = docX ? TemplateDocxConfig : TemplateDotxConfig;

                        //Set project and product name
                        docUtils.SetElementValue(TIDs.ProductName, curProd.Name);
                        docUtils.SetElementValue(TIDs.ProjectName, ProjectName);

                        //Set company and author
                        docUtils.SetElementValue(TIDs.ProjectCompanyName, Company);
                        docUtils.SetElementValue(TIDs.ProductAuthorName, Author);

                        //Set discipline
                        if (productDisciplineAssoc.Length == 0)
                            _Log.AddEntry("Product " + curProd.Name + " has no set discipline.", LogEntryType.Warning);
                        else
                            docUtils.SetElementValue(TIDs.PrGroup, ((Association<IArtifact, IDiscipline>)productDisciplineAssoc[0]).Destination.Name);

                        #region Set responsibles
                        ///TODO: Multiple responsibles correct?

                        //Build responsibles string
                        var roleToProductAssocs = processProvider.GetAssociationsWithDestination<IRole, IArtifact>(curProd);
                        var responsiblesStringBuilder = new StringBuilder();
                        foreach (var curRtoP in roleToProductAssocs)
                        {
                            if (!(curRtoP is RoleIsResponsibleForArtifact))
                                continue;

                            if (responsiblesStringBuilder.Length > 0)
                                responsiblesStringBuilder.Append(", ");
                            responsiblesStringBuilder.Append(((Association<IRole, IArtifact>)curRtoP).Name);
                        }
                        if (responsiblesStringBuilder.Length > 0)
                            docUtils.SetElementValue(TIDs.PrResponsible, responsiblesStringBuilder.ToString());
                        else
                            _Log.AddEntry(curProd.Name + ": Product " + curProd.Name + " has no one responsible.", LogEntryType.Warning); 
                        #endregion

                        #region Set involved roles
                        ///TODO: Bereits geschriebene Rollenverantwortliche als Mitbearbeiter nochmals nennen?

                        try
                        {
                            ///TODO: Randfall beachten: Keine Rollen -> Dokument lässt sich nicht öffnen, da Spalte komplett leer
                            foreach (var curRtoP in roleToProductAssocs)
                            {
                                if (!(curRtoP is RoleIsParticipatingInArtifact))
                                    continue;

                                var actorsUtils = new DocumentUtils(docUtils.MainDocPart, docUtils.CloneSection(TIDs.ActorsBegin, TIDs.ActorsEnd));
                                actorsUtils.SetElementValue(TIDs.ActorName, ((Association<IRole, IArtifact>) curRtoP).Source.Name);
                            }
                            if (roleToProductAssocs.Length == 0)
                                _Log.AddEntry(string.Format("Product {0} has no associated roles.", curProd.Name), LogEntryType.Warning);

                            docUtils.RemoveSection(TIDs.ActorsBegin, TIDs.ActorsEnd);
                        }
                        catch (CloneSectionException e)
                        {
                            _Log.AddEntry(e.Message, LogEntryType.Warning);
                        }
                        #endregion

                        #region Set creation
                        try
                        {
                            if (curProd.IsExternal)
                                (new DocumentUtils(docUtils.MainDocPart, docUtils.CloneSection(TIDs.CreationAttBegin, TIDs.CreationAttEnd))).SetElementValue(TIDs.CreationAtt,
                                                                                                                                       Language.Output_External);
                            if (curProd.IsInitial)
                                (new DocumentUtils(docUtils.MainDocPart, docUtils.CloneSection(TIDs.CreationAttBegin, TIDs.CreationAttEnd))).SetElementValue(TIDs.CreationAtt,
                                                                                                                                       Language.Output_Initial);
                            docUtils.RemoveSection(TIDs.CreationAttBegin, TIDs.CreationAttEnd);
                        }
                        catch (CloneSectionException e)
                        {
                            _Log.AddEntry(e.Message, LogEntryType.Warning);
                        }
                        #endregion

                        //Write creation section
                        SetProductCreationDependencies(docUtils, curProd, processProvider);

                        //Write topics
                        WriteProductTopics(docUtils, curProd, curProdItem.Value, processProvider);

                        //Write content dependencies
                        WriteProductContentDependencies(docUtils, curProd, processProvider);

                        //Clean document up
                        docUtils.CleanUpElements();

                        //Save
                        mainPart.Document.Save();
                        foreach (HeaderPart curHeaderPart in mainPart.HeaderParts)
                            curHeaderPart.Header.Save();
                    }
                    stream.Close();
                }
                catch (DirectoryNotFoundException ex)
                {
                    _Log.AddEntry(ex.Message);
                    return;
                }
                catch (UnauthorizedAccessException)
                {
                    _Log.AddEntry("Directory not accessable. Process Cancelling!");
                    return;
                }

                string destFilePath = OutputPath;
                if (productDisciplineAssoc.Length > 0)
                    destFilePath += NormalizeFileName(((Association<IArtifact, IDiscipline>)productDisciplineAssoc[0]).Destination.Name) + "/";

                MoveDocumentFile(tempDocumentPath, destFilePath + NormalizeFileName(curProd.Name) + (docX ? ".docx" : ".dotx"));
            }
        }

        /// <summary>
        /// Adds an extended attribute for DocX-Gen-specific tags. Furthermore, field tags, surrounding these specific tags, are removed.
        /// 
        /// This must be done so that MS Word does not recognize PET tags as fields. If this step is omitted Word only displays tag names instead of content in document templates (dotx files).
        /// However, field elements in SDT blocks must be ignored, so that e.g. table of contents are not influenced.
        /// </summary>
        /// <param name="root">Root element.</param>
        private static void CreateTagAttributes(OpenXmlElement root)
        {
            List<OpenXmlElement> simpleFieldsToBeReplaced = new List<OpenXmlElement>();
            List<OpenXmlElement> complexFieldsToBeReplaced = new List<OpenXmlElement>();
            bool inComplexField = false;
            CreateTagAttributes_Internal(root, ref simpleFieldsToBeReplaced, ref complexFieldsToBeReplaced, ref inComplexField);

            //Remove simple field elements
            foreach (OpenXmlElement curFieldElement in simpleFieldsToBeReplaced)
            {
                foreach (OpenXmlElement curFieldChild in curFieldElement.ChildElements)
                    curFieldElement.Parent.InsertBefore(curFieldChild.CloneNode(true), curFieldElement);
                curFieldElement.Remove();
            }

            //Remove complex field elements
            foreach (OpenXmlElement curFieldElement in complexFieldsToBeReplaced)
                curFieldElement.Remove();
        }

        /// <summary>
        /// Marks PET-special elements with a certain attribute and searches for form fields to be replaced.
        /// </summary>
        /// <param name="root">Element of which child elements have to be processed.</param>
        /// <param name="simpleFieldsToBeReplaced">Output: All simple field elements that have to be removed -> content must be preserved.</param>
        /// <param name="complexFieldsToBeReplaced">Output: All complex field elements that have to be removed -> content must be removed.</param>
        /// <param name="inComplexField">Temporary value indicating, whether a complex field is being processed at the moment.</param>
        private static void CreateTagAttributes_Internal(OpenXmlElement root, ref List<OpenXmlElement> simpleFieldsToBeReplaced, ref List<OpenXmlElement> complexFieldsToBeReplaced, ref bool inComplexField)
        {
            var registeredTags = GetRegisteredTags();

            foreach (OpenXmlElement childElement in root.ChildElements)
            {
                if (childElement is SdtBlock)
                {
                    //Ignore SDT blocks, so that e.g. table of contents is not influenced.
                    continue;
                }

                var childTag = childElement.InnerXml.ToUpper();
                if (registeredTags.Contains(childTag)) //Element is a tag
                {
                    //Add pet attribute
                    childElement.ExtendedAttributes.Add(new OpenXmlAttribute(DocumentUtils.NamespacePrefix, DocumentUtils.TagAttributeName,
                                                                             DocumentUtils.NamespaceUrl, childTag));

                    //If it is surrounded by a field tag -> add it for removal
                    OpenXmlElement curParentNode = childElement.Parent;
                    while(curParentNode != null)
                    {
                        if(curParentNode is SimpleField)
                        {
                            simpleFieldsToBeReplaced.Add(curParentNode);
                            break;
                        }
                        curParentNode = curParentNode.Parent;
                    }
                }

                if (childElement is FieldChar)
                {
                    var fieldChar = (FieldChar)childElement;
                    if (fieldChar.ComplexFieldCharType == ComplexFieldCharType.Begin)
                        inComplexField = true;

                    if (fieldChar.ComplexFieldCharType == ComplexFieldCharType.Separate || fieldChar.ComplexFieldCharType == ComplexFieldCharType.End)
                    {
                        complexFieldsToBeReplaced.Add(root);
                        inComplexField = false;
                    }
                }

                CreateTagAttributes_Internal(childElement, ref simpleFieldsToBeReplaced, ref complexFieldsToBeReplaced, ref inComplexField);
            }

            if(inComplexField)
                complexFieldsToBeReplaced.Add(root);
        }

        private static ICollection<string> GetRegisteredTags()
        {
            //Create list of all existing PET element tags
            var registeredTags = new HashSet<string>();

            var tmp = TIDs.ProjectName; //Initialize TIDs resource
            ResourceSet set = TIDs.ResourceManager.GetResourceSet(CultureInfo.CurrentCulture, true, true);
            foreach (DictionaryEntry o in set)
                registeredTags.Add((string)o.Value);
            //Add sub-paragraph tags
            for (int i = 1; i <= MaxSubParagraphLevel; i++)
            {
                registeredTags.Add(string.Format(TIDs.SubContentsBegin, i));
                registeredTags.Add(string.Format(TIDs.SubContentsEnd, i));
                registeredTags.Add(string.Format(TIDs.SubContentName, i));
                registeredTags.Add(string.Format(TIDs.SubContentDescription, i));
                registeredTags.Add(string.Format(TIDs.SubContentTemplate, i));
                registeredTags.Add(string.Format(TIDs.SubContentTemplateBegin, i));
                registeredTags.Add(string.Format(TIDs.SubContentTemplateEnd, i));
            }
            return registeredTags;
        }


        /// <summary>
        /// Sets a named property of the document.
        /// Inspired by http://www.notesfor.net/post/2009/09/16/Add-and-update-custom-document-properties-in-a-docx-.aspx
        /// </summary>
        private static void SetDocumentProperty(WordprocessingDocument package, string propName, string propValue)
        {
            int nextPropertyId = 2; // 2 is the minimum ID set by MS Office. Don't have a clue why they don't start at 0.
            CustomFilePropertiesPart customPropertiesPart = package.CustomFilePropertiesPart;

            if (customPropertiesPart == null)
            {
                customPropertiesPart = package.AddCustomFilePropertiesPart();
                new DocumentFormat.OpenXml.CustomProperties.Properties().Save(customPropertiesPart);
            }
            else
            {
                // In order to add properties in the document, we need to assign an unique id
                // to each Property object. So we'll loop through all of the existing <op:property> elements
                // to find the highest Id, then increment it for each new property.
                foreach (var p in package.CustomFilePropertiesPart.Properties.Elements<DocumentFormat.OpenXml.CustomProperties.Property>())
                {
                    if (p.PropertyId.Value > nextPropertyId) nextPropertyId = p.PropertyId;
                }
                if(nextPropertyId > 2) nextPropertyId++;
            }


            //// Get back all the custom properties contained in this document.
            var knownCustomProperties = new Dictionary<String, DocumentFormat.OpenXml.CustomProperties.Property>();
            foreach (var p in customPropertiesPart.Properties.Elements<DocumentFormat.OpenXml.CustomProperties.Property>())
            {
                knownCustomProperties.Add(p.Name, p);
            }

            customPropertiesPart.Properties.Append(new DocumentFormat.OpenXml.CustomProperties.Property(
                new VTLPWSTR(propValue)
            ) { FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}", PropertyId = nextPropertyId, Name = propName });

            customPropertiesPart.Properties.Save();
        }

        /// <summary>
        /// Copies the template docx or the dotx file and .
        /// </summary>
        /// <param name="strPath">path to create the copy of the template.</param>
        /// <param name="dotX">if set to <c>true</c> copy of the dotx template created </param>
        /// <returns></returns>
        private string CopyTemplate(string path, bool docX)
        {
            string masterPath;
            if (docX)
                masterPath = TemplateDocxPath;
            else
                masterPath = TemplateDotxPath;

            path +=  "TEMPORARY" + (docX ? ".docx" : ".dotx");

            try
            {
                try
                {
                    for (var counter = 2; counter < 1000; counter++)
                    {
                        if (!File.Exists(path))
                        {
                            File.Copy(masterPath, path);
                            return path;
                        }

                        if (path.Contains("(") && path.Contains(")") && path.IndexOf(")") > 0)
                        {
                            path = path.Remove(path.IndexOf("("), 3);
                        }
                        path = path.Insert(path.LastIndexOf("."), string.Format("({0})", counter));
                    }
                    _Log.AddEntry("Filename is taken. " + path);
                    return null;
                }
                catch (DirectoryNotFoundException)
                {
                    _Log.AddEntry("Directory found. " + path);
                    return null;
                }
            }
            catch (UnauthorizedAccessException)
            {
                _Log.AddEntry("Cannot access file. " + path);
                return null;
            }
        }

        /// <summary>
        /// The method ArangeFileLocationAndName builds the right folder structure and gives to the documents the right name.
        /// </summary>
        /// <param name="sourceFilePath">file path.</param>
        /// <param name="names">array with th names.</param>
        private static void MoveDocumentFile(string sourceFilePath, string destinationFilePath)
        {
            try
            {
                var fi = new FileInfo(destinationFilePath);
                if(fi.Directory != null)
                    fi.Directory.Create();
            }
            catch (IOException)
            {
                return;
            }

            try
            {
                File.Move(sourceFilePath, destinationFilePath);
            }
            catch (IOException)
            {
                var i = 1;
                while (i < 1000)
                {
                    string newPath = destinationFilePath.Insert(destinationFilePath.LastIndexOf('.'), " (" + i + ")");
                    if (!File.Exists(newPath))
                    {
                        try
                        {
                            File.Move(sourceFilePath, newPath);
                            break;
                        }
                        catch(IOException)
                        {}
                    }
                    i++;
                }
            }
        }

        /// <summary>
        /// NormalizeFileName removes all possible not allowed characters for the file name
        /// </summary>
        /// <param name="fileName">Name of the file.</param>
        /// <returns></returns>
        private static string NormalizeFileName(string fileName)
        {
            string strResult = fileName;
            string[] notAllowedChars = {"/", @"\", ":", "|", "*", "?", "<", ">"};
            fileName.TrimEnd();
            foreach (string t in notAllowedChars)
            {
                if (fileName.Contains(t))
                {
                    strResult = fileName.Replace(t, " ");
                    return strResult.TrimEnd();
                }
            }
            return strResult;
        }

        private class CollectedProductContentDependency
        {
            public string Name;
            public string Description;
            public readonly List<string> DependentProductNames = new List<string>();
        }

        /// <summary>
        /// Gets an identifier for an association. Is tolerant to identifiers not set.
        /// </summary>
        /// <param name="association">Association.</param>
        /// <returns>Identifier for the association.</returns>
        private static string BuildSafeAssociationId(IAssociation association)
        {
            return !string.IsNullOrEmpty(association.Id) ? association.Id : association.SourceId + association.DestinationId;
        }

        private void WriteProductContentDependencies(DocumentUtils utils, IArtifact product, IProcessProvider processProvider)
        {
            var collectedDependencies = new Dictionary<string, CollectedProductContentDependency>();

            foreach (Association<IArtifact, IArtifact> curDep in processProvider.GetAssociationsWithSource<IArtifact, IArtifact>(product))
            {
                if (curDep is ContentDependency)
                {
                    CollectedProductContentDependency curCollectedDependency;
                    var depId = BuildSafeAssociationId(curDep);
                    if (collectedDependencies.ContainsKey(depId))
                        curCollectedDependency = collectedDependencies[depId];
                    else
                    {
                        curCollectedDependency = new CollectedProductContentDependency { Name = curDep.Name };

                        if (curDep.ExtendedData.ContainsKey("DescriptionRaw") && curDep.ExtendedData["DescriptionRaw"] is string)
                            curCollectedDependency.Description = curDep.ExtendedData["DescriptionRaw"] as string;
                        else
                            curCollectedDependency.Description = curDep.Description;

                        curCollectedDependency.DependentProductNames.Add(product.Name);
                        collectedDependencies.Add(depId, curCollectedDependency);
                    }

                    curCollectedDependency.DependentProductNames.Add(processProvider.GetArtifactByID(curDep.DestinationId).Name);
                }
            }

            try
            {
                foreach (var curContentDependency in collectedDependencies)
                {
                    var dependencyUtils = new DocumentUtils(utils.MainDocPart, utils.CloneSection(TIDs.DependenciesBegin, TIDs.DependenciesEnd));
                    dependencyUtils.SetElementValue(TIDs.DependencyName, curContentDependency.Value.Name);
                    dependencyUtils.SetElementValue(TIDs.DependencyDescription, ParseFormatedText(curContentDependency.Value.Description));

                    foreach (var curDepProd in curContentDependency.Value.DependentProductNames)
                    {
                        var depProdUtils = new DocumentUtils(utils.MainDocPart, dependencyUtils.CloneSection(TIDs.DependentPrBegin, TIDs.DependentPrEnd));
                        depProdUtils.SetElementValue(TIDs.DependentPrName, curDepProd);
                    }
                    dependencyUtils.RemoveSection(TIDs.DependentPrBegin, TIDs.DependentPrEnd);
                }
                utils.RemoveSection(TIDs.DependenciesBegin, TIDs.DependenciesEnd);
            }
            catch (CloneSectionException e)
            {
                //Might happen, when a sub-paragraph tag does not exist
                _Log.AddEntry(e.Message, LogEntryType.Warning);
            }
        }

        /// <summary>
        /// Writes "Creation" section of document. Writes all products, that result in the generation of this product.
        /// </summary>
        /// <param name="docUtils"></param>
        /// <param name="product"></param>
        /// <param name="processProvider"></param>
        private void SetProductCreationDependencies(DocumentUtils docUtils, IArtifact product, IProcessProvider processProvider)
        {
            if (!product.IsInitial) //Initial products have no "creation"
            {
                //Dictionary of dependency names -> list of creating products
                var creatingProductsDictionary = new Dictionary<string, List<string>>();
                foreach (Association<IArtifact, IArtifact> curDep in processProvider.GetAssociationsWithDestination<IArtifact, IArtifact>(product))
                {
                    if (curDep is GenerationDependency)
                    {
                        List<string> creatingProdList;
                        if (creatingProductsDictionary.ContainsKey(curDep.Name))
                            creatingProdList = creatingProductsDictionary[curDep.Name];
                        else
                        {
                            creatingProdList = new List<string>();
                            creatingProductsDictionary.Add(curDep.Name, creatingProdList);
                        }
                        creatingProdList.Add(curDep.Source.Name);
                    }
                }

                foreach (KeyValuePair<string, List<string>> curDependencyEntry in creatingProductsDictionary)
                {
                    try
                    {
                        var curDependencyUtils = new DocumentUtils(docUtils.MainDocPart, docUtils.CloneSection(TIDs.CreationBegin, TIDs.CreationEnd));
                        curDependencyUtils.SetElementValue(TIDs.CreationName, curDependencyEntry.Key);

                        foreach (string curDependentProductName in curDependencyEntry.Value)
                        {
                            var curDependentProdUtils = new DocumentUtils(docUtils.MainDocPart, curDependencyUtils.CloneSection(TIDs.CreationSourceBegin, TIDs.CreationSourceEnd));
                            curDependentProdUtils.SetElementValue(TIDs.CreationOtherProduct, curDependentProductName);
                        }
                        curDependencyUtils.RemoveSection(TIDs.CreationSourceBegin, TIDs.CreationSourceEnd);
                    }
                    catch (CloneSectionException e)
                    {
                        _Log.AddEntry(e.Message, LogEntryType.Warning);
                        return;
                    }
                }
            }

            try
            {
                docUtils.RemoveSection(TIDs.CreationBegin, TIDs.CreationEnd);
            }
            catch (CloneSectionException e)
            {
                _Log.AddEntry(e.Message, LogEntryType.Warning);
            }
        }

        private void WriteProductTopics(DocumentUtils utils, IArtifact product, Tree<TemplateSerializationItem> templatesToBeWritten, IProcessProvider processProvider)
        {
            //Load Mustertexte parser
            TemplateParser templateParser = null;
            if (File.Exists(TemplateTextsPath))
                templateParser = TemplateParser.CreateTemplateParser(TemplateTextsPath, product.Name);
            
            //Get topics
            var topicList = GetTopics(product, null, processProvider);
            try
            {
                foreach (TopicEntry curTopicEntry in topicList)
                {
                    //Check if this topic is to be written
                    TreeItem<TemplateSerializationItem> belongingTemplateTreeItem = GetTemplateElementToBeWritten<TemplateSerializationTopic>(curTopicEntry.Topic.Name, templatesToBeWritten);
                    if (belongingTemplateTreeItem == null)
                        continue;

                    //Write topic
                    var topicUtils = new DocumentUtils(utils.MainDocPart, utils.CloneSection(TIDs.ContentsBegin, TIDs.ContentsEnd));
                    topicUtils.TemplateDocumentConfig = utils.TemplateDocumentConfig;

                    topicUtils.SetElementValue(TIDs.ContentName, curTopicEntry.Topic.Name);
                    topicUtils.SetElementValue(TIDs.ContentDescription, ParseTopicDescription(curTopicEntry.Topic));

                    //Mustertext
                    ProductTopic topicTexts = templateParser != null ? templateParser.GetTopicByName(curTopicEntry.Topic.Name) : null;
                    WriteSingleTopics(topicUtils, curTopicEntry.Topic, belongingTemplateTreeItem, topicTexts, 1, processProvider);
                }
                utils.RemoveSection(TIDs.ContentsBegin, TIDs.ContentsEnd);
            }
            catch (CloneSectionException e)
            {
                //Might happen, when a sub-paragraph tag does not exist
                _Log.AddEntry(e.Message, LogEntryType.Warning);
            }
        }

        private static TreeItem<TemplateSerializationItem> GetTemplateElementToBeWritten<T>(string name, TreeItem<TemplateSerializationItem> templatesToBeWritten) where T : TemplateSerializationItem
        {
            foreach (var curProductTopicToBeWritten in templatesToBeWritten.Children)
            {
                if (curProductTopicToBeWritten.Value is T && curProductTopicToBeWritten.Value.Name.Equals(name))
                    return curProductTopicToBeWritten;
            }
            return null;
        }

        internal static TopicEntry[] GetTopics(IProcessElement parentElement, TemplateTopic template, IProcessProvider processProvider)
        {
            var topicList = new SortedList<int, TopicEntry>();
            ITopic[] topics;

            if (parentElement is IArtifact)
            {
                var topicAssociations = processProvider.GetAssociationsWithDestination<ITopic, IArtifact>((IArtifact)parentElement);
                topics = new ITopic[topicAssociations.Length];
                for (int a = 0; a < topicAssociations.Length; a++)
                    topics[a] = ((Association<ITopic, IArtifact>)topicAssociations[a]).Source;
            }
            else if (parentElement is ITopic)
            {
                topics = processProvider.GetDirectSubTopics((ITopic)parentElement);
            }
            else
                return null;

            foreach (ITopic curTopic in topics)
            {
                TopicEntry entry = new TopicEntry { Topic = curTopic };
                if(template != null)
                {
                    foreach (AdditionalProductTopic curAddTopic in template.AdditionalTopics)
                    {
                        if (curAddTopic.Name.Equals(curTopic.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            entry.Template = curAddTopic;
                            break;
                        }
                    }
                }
                topicList.Add(GetUniqueSortedListId(topicList, curTopic.Number), entry);
            }

            TopicEntry[] retArray = new TopicEntry[topicList.Count];
            int curPos = 0;
            foreach (var curEntry in topicList)
            {
                retArray[curPos] = curEntry.Value;
                curPos++;
            }
            return retArray;
        }

        private void WriteSingleTopics(DocumentUtils utils, ITopic topic, TreeItem<TemplateSerializationItem> templatesToBeWritten, TemplateTopic template, int subTopicLevel, IProcessProvider processProvider)
        {
            //Build tag names
            string subtopicBeginElement = string.Format(TIDs.SubContentsBegin, subTopicLevel);
            string subtopicEndElement = string.Format(TIDs.SubContentsEnd, subTopicLevel);

            string templateTextBeginElementName, templateTextEndElementName, templateTextContentElementName;
            if (subTopicLevel == 1)
            {
                templateTextBeginElementName = TIDs.ContentTemplateBegin;
                templateTextEndElementName = TIDs.ContentTemplateEnd;
                templateTextContentElementName = TIDs.ContentTemplate;
            }
            else
            {
                templateTextBeginElementName = string.Format(TIDs.SubContentTemplateBegin, subTopicLevel - 1);
                templateTextEndElementName = string.Format(TIDs.SubContentTemplateEnd, subTopicLevel - 1);
                templateTextContentElementName = string.Format(TIDs.SubContentTemplate, subTopicLevel - 1);
            }

            try
            {
                if (template == null)
                    return;

                //Write topic template text
                try
                {
                    if (template.Templates != null)
                    {
                        foreach (TextTemplate curText in template.Templates)
                        {
                            //Check if this text is to be written
                            if (GetTemplateElementToBeWritten<TemplateSerializationText>(curText.Name, templatesToBeWritten) == null)
                                continue;

                            //Write text
                            var templateTextUtils = new DocumentUtils(utils.MainDocPart, utils.CloneSection(templateTextBeginElementName, templateTextEndElementName));
                            templateTextUtils.TemplateDocumentConfig = utils.TemplateDocumentConfig;

                            templateTextUtils.SetElementValue(templateTextContentElementName, curText);
                        }
                    }
                }
                catch (CloneSectionException)
                {
                }


                if (subTopicLevel > MaxSubParagraphLevel)
                    return;

                //Build sorted subtopic list
                var topicList = GetTopics(topic, template, processProvider);

                //Write subtopics
                string subContentNameElement = string.Format(TIDs.SubContentName, subTopicLevel);
                string subContentDescriptionElement = string.Format(TIDs.SubContentDescription, subTopicLevel);
                try
                {
                    foreach (TopicEntry curSubTopicEntry in topicList)
                    {
                        //Check if this topic is to be written
                        TreeItem<TemplateSerializationItem> belongingTemplateTreeItem = GetTemplateElementToBeWritten<TemplateSerializationTopic>(curSubTopicEntry.Topic.Name, templatesToBeWritten);
                        if (belongingTemplateTreeItem == null)
                            continue;

                        //Write
                        var subTopicUtils = new DocumentUtils(utils.MainDocPart, utils.CloneSection(subtopicBeginElement, subtopicEndElement));
                        subTopicUtils.TemplateDocumentConfig = utils.TemplateDocumentConfig;

                        subTopicUtils.SetElementValue(subContentNameElement,
                                                      curSubTopicEntry.Topic != null
                                                          ? curSubTopicEntry.Topic.Name
                                                          : curSubTopicEntry.Template.Name);
                        if (curSubTopicEntry.Topic != null)
                            subTopicUtils.SetElementValue(subContentDescriptionElement, ParseTopicDescription(curSubTopicEntry.Topic));
                        else
                            subTopicUtils.SetElementValue(subContentDescriptionElement, string.Empty);

                        WriteSingleTopics(subTopicUtils, curSubTopicEntry.Topic, belongingTemplateTreeItem, curSubTopicEntry.Template, subTopicLevel + 1, processProvider);
                    }
                }
                catch (CloneSectionException)
                {
                    //Might happen, when a sub-paragraph tag does not exist. No problem.
                }
            }
            finally
            {
                try
                {
                    utils.RemoveSection(subtopicBeginElement, subtopicEndElement);
                    utils.RemoveSection(templateTextBeginElementName, templateTextEndElementName);
                }
                catch (CloneSectionException)
                {
                    //No problem
                }
            }
        }

        private TextTemplate ParseTopicDescription(ITopic topic)
        {
            object text = null;
            topic.ExtendedData.TryGetValue("DescriptionRaw", out text);
            if(string.IsNullOrEmpty((string)text))
                text = topic.Description;
            return ParseFormatedText((string)text);
        }

        private TextTemplate ParseFormatedText(string text)
        {
            XmlDocument doc = new XmlDocument();
            doc.LoadXml("<d><Text>" + text + "</Text></d>");
            return new TextTemplate(TemplateTextsPath, doc.FirstChild);
        }

        private static int GetUniqueSortedListId(SortedList<int, TopicEntry> list, int suggestedId)
        {
            int curId = suggestedId;
            while (list.ContainsKey(curId))
                curId++;
            return curId;
        }
    }

    internal struct TopicEntry
    {
        public ITopic Topic;
        public TemplateTopic Template;
    }
}